/**
 * Copyright 2010-present Facebook.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook;

import com.facebook.android.R;
import com.facebook.internal.Utility;
import org.json.JSONException;
import org.json.JSONObject;

import java.net.HttpURLConnection;

/**
 * This class represents an error that occurred during a Facebook request.
 * <p/>
 * In general, one would call {@link #getCategory()} to determine the type
 * of error that occurred, and act accordingly. The app can also call
 * {@link #getUserActionMessageId()} in order to get the resource id for a
 * string that can be displayed to the user. For more information on error
 * handling, see <a href="https://developers.facebook.com/docs/reference/api/errors/">
 * https://developers.facebook.com/docs/reference/api/errors/</a>
 */
public final class FacebookRequestError
{

  /**
   * Represents an invalid or unknown error code from the server.
   */
  public static final int INVALID_ERROR_CODE = -1;

  /**
   * Indicates that there was no valid HTTP status code returned, indicating
   * that either the error occurred locally, before the request was sent, or
   * that something went wrong with the HTTP connection. Check the exception
   * from {@link #getException()};
   */
  public static final int INVALID_HTTP_STATUS_CODE = -1;

  private static final int INVALID_MESSAGE_ID = 0;

  private static final String CODE_KEY = "code";
  private static final String BODY_KEY = "body";
  private static final String ERROR_KEY = "error";
  private static final String ERROR_TYPE_FIELD_KEY = "type";
  private static final String ERROR_CODE_FIELD_KEY = "code";
  private static final String ERROR_MESSAGE_FIELD_KEY = "message";
  private static final String ERROR_CODE_KEY = "error_code";
  private static final String ERROR_SUB_CODE_KEY = "error_subcode";
  private static final String ERROR_MSG_KEY = "error_msg";
  private static final String ERROR_REASON_KEY = "error_reason";

  private static class Range
  {
    private final int start, end;

    private Range(int start, int end)
    {
      this.start = start;
      this.end = end;
    }

    boolean contains(int value)
    {
      return start <= value && value <= end;
    }
  }

  private static final int EC_UNKNOWN_ERROR = 1;
  private static final int EC_SERVICE_UNAVAILABLE = 2;
  private static final int EC_APP_TOO_MANY_CALLS = 4;
  private static final int EC_USER_TOO_MANY_CALLS = 17;
  private static final int EC_PERMISSION_DENIED = 10;
  private static final int EC_INVALID_SESSION = 102;
  private static final int EC_INVALID_TOKEN = 190;
  private static final Range EC_RANGE_PERMISSION = new Range(200, 299);
  private static final int EC_APP_NOT_INSTALLED = 458;
  private static final int EC_USER_CHECKPOINTED = 459;
  private static final int EC_PASSWORD_CHANGED = 460;
  private static final int EC_EXPIRED = 463;
  private static final int EC_UNCONFIRMED_USER = 464;

  private static final Range HTTP_RANGE_SUCCESS = new Range(200, 299);
  private static final Range HTTP_RANGE_CLIENT_ERROR = new Range(400, 499);
  private static final Range HTTP_RANGE_SERVER_ERROR = new Range(500, 599);

  private final int userActionMessageId;
  private final boolean shouldNotifyUser;
  private final Category category;
  private final int requestStatusCode;
  private final int errorCode;
  private final int subErrorCode;
  private final String errorType;
  private final String errorMessage;
  private final JSONObject requestResult;
  private final JSONObject requestResultBody;
  private final Object batchRequestResult;
  private final HttpURLConnection connection;
  private final FacebookException exception;

  private FacebookRequestError(int requestStatusCode, int errorCode,
                               int subErrorCode, String errorType, String errorMessage, JSONObject requestResultBody,
                               JSONObject requestResult, Object batchRequestResult, HttpURLConnection connection,
                               FacebookException exception)
  {
    this.requestStatusCode = requestStatusCode;
    this.errorCode = errorCode;
    this.subErrorCode = subErrorCode;
    this.errorType = errorType;
    this.errorMessage = errorMessage;
    this.requestResultBody = requestResultBody;
    this.requestResult = requestResult;
    this.batchRequestResult = batchRequestResult;
    this.connection = connection;

    boolean isLocalException = false;
    if (exception != null)
    {
      this.exception = exception;
      isLocalException = true;
    }
    else
    {
      this.exception = new FacebookServiceException(this, errorMessage);
    }

    // Initializes the error categories based on the documented error codes as outlined here
    // https://developers.facebook.com/docs/reference/api/errors/
    Category errorCategory = null;
    int messageId = INVALID_MESSAGE_ID;
    boolean shouldNotify = false;
    if (isLocalException)
    {
      errorCategory = Category.CLIENT;
      messageId = INVALID_MESSAGE_ID;
    }
    else
    {
      if (errorCode == EC_UNKNOWN_ERROR || errorCode == EC_SERVICE_UNAVAILABLE)
      {
        errorCategory = Category.SERVER;
      }
      else
        if (errorCode == EC_APP_TOO_MANY_CALLS || errorCode == EC_USER_TOO_MANY_CALLS)
        {
          errorCategory = Category.THROTTLING;
        }
        else
          if (errorCode == EC_PERMISSION_DENIED || EC_RANGE_PERMISSION.contains(errorCode))
          {
            errorCategory = Category.PERMISSION;
            messageId = R.string.com_facebook_requesterror_permissions;
          }
          else
            if (errorCode == EC_INVALID_SESSION || errorCode == EC_INVALID_TOKEN)
            {
              if (subErrorCode == EC_USER_CHECKPOINTED || subErrorCode == EC_UNCONFIRMED_USER)
              {
                errorCategory = Category.AUTHENTICATION_RETRY;
                messageId = R.string.com_facebook_requesterror_web_login;
                shouldNotify = true;
              }
              else
              {
                errorCategory = Category.AUTHENTICATION_REOPEN_SESSION;

                if ((subErrorCode == EC_APP_NOT_INSTALLED) || (subErrorCode == EC_EXPIRED))
                {
                  messageId = R.string.com_facebook_requesterror_relogin;
                }
                else
                  if (subErrorCode == EC_PASSWORD_CHANGED)
                  {
                    messageId = R.string.com_facebook_requesterror_password_changed;
                  }
                  else
                  {
                    messageId = R.string.com_facebook_requesterror_reconnect;
                    shouldNotify = true;
                  }
              }
            }

      if (errorCategory == null)
      {
        if (HTTP_RANGE_CLIENT_ERROR.contains(requestStatusCode))
        {
          errorCategory = Category.BAD_REQUEST;
        }
        else
          if (HTTP_RANGE_SERVER_ERROR.contains(requestStatusCode))
          {
            errorCategory = Category.SERVER;
          }
          else
          {
            errorCategory = Category.OTHER;
          }
      }
    }

    this.category = errorCategory;
    this.userActionMessageId = messageId;
    this.shouldNotifyUser = shouldNotify;
  }

  private FacebookRequestError(int requestStatusCode, int errorCode,
                               int subErrorCode, String errorType, String errorMessage, JSONObject requestResultBody,
                               JSONObject requestResult, Object batchRequestResult, HttpURLConnection connection)
  {
    this(requestStatusCode, errorCode, subErrorCode, errorType, errorMessage,
        requestResultBody, requestResult, batchRequestResult, connection, null);
  }

  FacebookRequestError(HttpURLConnection connection, Exception exception)
  {
    this(INVALID_HTTP_STATUS_CODE, INVALID_ERROR_CODE, INVALID_ERROR_CODE,
        null, null, null, null, null, connection,
        (exception instanceof FacebookException) ?
            (FacebookException) exception : new FacebookException(exception));
  }

  public FacebookRequestError(int errorCode, String errorType, String errorMessage)
  {
    this(INVALID_HTTP_STATUS_CODE, errorCode, INVALID_ERROR_CODE, errorType, errorMessage,
        null, null, null, null, null);
  }

  /**
   * Returns the resource id for a user-friendly message for the application to
   * present to the user.
   *
   * @return a user-friendly message to present to the user
   */
  public int getUserActionMessageId()
  {
    return userActionMessageId;
  }

  /**
   * Returns whether direct user action is required to successfully continue with the Facebook
   * operation. If user action is required, apps can also call {@link #getUserActionMessageId()}
   * in order to get a resource id for a message to show the user.
   *
   * @return whether direct user action is required
   */
  public boolean shouldNotifyUser()
  {
    return shouldNotifyUser;
  }

  /**
   * Returns the category in which the error belongs. Applications can use the category
   * to determine how best to handle the errors (e.g. exponential backoff for retries if
   * being throttled).
   *
   * @return the category in which the error belong
   */
  public Category getCategory()
  {
    return category;
  }

  /**
   * Returns the HTTP status code for this particular request.
   *
   * @return the HTTP status code for the request
   */
  public int getRequestStatusCode()
  {
    return requestStatusCode;
  }

  /**
   * Returns the error code returned from Facebook.
   *
   * @return the error code returned from Facebook
   */
  public int getErrorCode()
  {
    return errorCode;
  }

  /**
   * Returns the sub-error code returned from Facebook.
   *
   * @return the sub-error code returned from Facebook
   */
  public int getSubErrorCode()
  {
    return subErrorCode;
  }

  /**
   * Returns the type of error as a raw string. This is generally less useful
   * than using the {@link #getCategory()} method, but can provide further details
   * on the error.
   *
   * @return the type of error as a raw string
   */
  public String getErrorType()
  {
    return errorType;
  }

  /**
   * Returns the error message returned from Facebook.
   *
   * @return the error message returned from Facebook
   */
  public String getErrorMessage()
  {
    if (errorMessage != null)
    {
      return errorMessage;
    }
    else
    {
      return exception.getLocalizedMessage();
    }
  }

  /**
   * Returns the body portion of the response corresponding to the request from Facebook.
   *
   * @return the body of the response for the request
   */
  public JSONObject getRequestResultBody()
  {
    return requestResultBody;
  }

  /**
   * Returns the full JSON response for the corresponding request. In a non-batch request,
   * this would be the raw response in the form of a JSON object. In a batch request, this
   * result will contain the body of the response as well as the HTTP headers that pertain
   * to the specific request (in the form of a "headers" JSONArray).
   *
   * @return the full JSON response for the request
   */
  public JSONObject getRequestResult()
  {
    return requestResult;
  }

  /**
   * Returns the full JSON response for the batch request. If the request was not a batch
   * request, then the result from this method is the same as {@link #getRequestResult()}.
   * In case of a batch request, the result will be a JSONArray where the elements
   * correspond to the requests in the batch. Callers should check the return type against
   * either JSONObject or JSONArray and cast accordingly.
   *
   * @return the full JSON response for the batch
   */
  public Object getBatchRequestResult()
  {
    return batchRequestResult;
  }

  /**
   * Returns the HTTP connection that was used to make the request.
   *
   * @return the HTTP connection used to make the request
   */
  public HttpURLConnection getConnection()
  {
    return connection;
  }

  /**
   * Returns the exception associated with this request, if any.
   *
   * @return the exception associated with this request
   */
  public FacebookException getException()
  {
    return exception;
  }

  @Override
  public String toString()
  {
    return new StringBuilder("{HttpStatus: ")
        .append(requestStatusCode)
        .append(", errorCode: ")
        .append(errorCode)
        .append(", errorType: ")
        .append(errorType)
        .append(", errorMessage: ")
        .append(getErrorMessage())
        .append("}")
        .toString();
  }

  static FacebookRequestError checkResponseAndCreateError(JSONObject singleResult,
                                                          Object batchResult, HttpURLConnection connection)
  {
    try
    {
      if (singleResult.has(CODE_KEY))
      {
        int responseCode = singleResult.getInt(CODE_KEY);
        Object body = Utility.getStringPropertyAsJSON(singleResult, BODY_KEY,
            Response.NON_JSON_RESPONSE_PROPERTY);

        if (body != null && body instanceof JSONObject)
        {
          JSONObject jsonBody = (JSONObject) body;
          // Does this response represent an error from the service? We might get either an "error"
          // with several sub-properties, or else one or more top-level fields containing error info.
          String errorType = null;
          String errorMessage = null;
          int errorCode = INVALID_ERROR_CODE;
          int errorSubCode = INVALID_ERROR_CODE;

          boolean hasError = false;
          if (jsonBody.has(ERROR_KEY))
          {
            // We assume the error object is correctly formatted.
            JSONObject error = (JSONObject) Utility.getStringPropertyAsJSON(jsonBody, ERROR_KEY, null);

            errorType = error.optString(ERROR_TYPE_FIELD_KEY, null);
            errorMessage = error.optString(ERROR_MESSAGE_FIELD_KEY, null);
            errorCode = error.optInt(ERROR_CODE_FIELD_KEY, INVALID_ERROR_CODE);
            errorSubCode = error.optInt(ERROR_SUB_CODE_KEY, INVALID_ERROR_CODE);
            hasError = true;
          }
          else
            if (jsonBody.has(ERROR_CODE_KEY) || jsonBody.has(ERROR_MSG_KEY)
                || jsonBody.has(ERROR_REASON_KEY))
            {
              errorType = jsonBody.optString(ERROR_REASON_KEY, null);
              errorMessage = jsonBody.optString(ERROR_MSG_KEY, null);
              errorCode = jsonBody.optInt(ERROR_CODE_KEY, INVALID_ERROR_CODE);
              errorSubCode = jsonBody.optInt(ERROR_SUB_CODE_KEY, INVALID_ERROR_CODE);
              hasError = true;
            }

          if (hasError)
          {
            return new FacebookRequestError(responseCode, errorCode, errorSubCode,
                errorType, errorMessage, jsonBody, singleResult, batchResult, connection);
          }
        }

        // If we didn't get error details, but we did get a failure response code, report it.
        if (!HTTP_RANGE_SUCCESS.contains(responseCode))
        {
          return new FacebookRequestError(responseCode, INVALID_ERROR_CODE,
              INVALID_ERROR_CODE, null, null,
              singleResult.has(BODY_KEY) ?
                  (JSONObject) Utility.getStringPropertyAsJSON(
                      singleResult, BODY_KEY, Response.NON_JSON_RESPONSE_PROPERTY) : null,
              singleResult, batchResult, connection);
        }
      }
    }
    catch (JSONException e)
    {
      // defer the throwing of a JSONException to the graph object proxy
    }
    return null;
  }

  /**
   * An enum that represents the Facebook SDK classification for the error that occurred.
   */
  public enum Category
  {
    /**
     * Indicates that the error is authentication related, and that the app should retry
     * the request after some user action.
     */
    AUTHENTICATION_RETRY,

    /**
     * Indicates that the error is authentication related, and that the app should close
     * the session and reopen it.
     */
    AUTHENTICATION_REOPEN_SESSION,

    /**
     * Indicates that the error is permission related.
     */
    PERMISSION,

    /**
     * Indicates that the error implies the server had an unexpected failure or may be
     * temporarily unavailable.
     */
    SERVER,

    /**
     * Indicates that the error results from the server throttling the client.
     */
    THROTTLING,

    /**
     * Indicates that the error is Facebook-related but cannot be categorized at this time,
     * and is likely newer than the current version of the SDK.
     */
    OTHER,

    /**
     * Indicates that the error is an application error resulting in a bad or malformed
     * request to the server.
     */
    BAD_REQUEST,

    /**
     * Indicates that this is a client-side error. Examples of this can include, but are
     * not limited to, JSON parsing errors or {@link java.io.IOException}s.
     */
    CLIENT
  }

  ;

}
